package opencm;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;


import org.apache.commons.lang3.builder.CompareToBuilder;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.LocalDate;

public final class ServicoAgendamento {

	private RepositorioConsulta repositorioConsulta;
	private RepositorioMedico repositorioMedico;

	public ServicoAgendamento() {
		this(Main.repositorioMedico, Main.repositorioConsulta);
	}

	public ServicoAgendamento(RepositorioMedico repositorioMedico, RepositorioConsulta repositorioConsulta) {
		this.repositorioMedico = repositorioMedico;
		this.repositorioConsulta = repositorioConsulta;
	}

	public Agendamento criarAgendamento(Consulta consulta, Medico medico, DateTime apartirDe)
			throws ErroAgendamento {
		LocalDate dataBase = apartirDe.toLocalDate();
		DiaSemana diaSemana = DiaSemana.fromDayOfWeek(dataBase.getDayOfWeek());
		Consulta.Tipo tipo = consulta.getTipo();

		List<Horario> horariosAtendimento = medico.getHorarios(tipo, diaSemana);
		if (horariosAtendimento.isEmpty()) {
			throw new IllegalArgumentException(String.format("O médico não atendimento %s na %s",
					tipo.toString(), diaSemana.toString()));
		}
		Set<Interval> intervalosAtendimento = new HashSet<Interval>();
		for (Horario h : horariosAtendimento) {
			intervalosAtendimento.add(h.toInterval(dataBase));
		}

		List<Consulta> consultasAgendadas = this.repositorioConsulta.pesquisar(medico, dataBase);
		Set<Interval> intervalosOcupados = new HashSet<Interval>();
		for (Consulta c : consultasAgendadas) {
			intervalosOcupados.add(c.getAgendamento().toInterval());
		}

		// Considera cirugias da mesma especialidade como intervalos ocupados.
		if (tipo.equals(Consulta.Tipo.CIRUGIA)) {
			List<Consulta> cirugiasEspecialidadeAgendadas = this.repositorioConsulta.pesquisarCirugias(
					medico.getEspecialidade(), dataBase);
			for (Consulta c : cirugiasEspecialidadeAgendadas) {
				intervalosOcupados.add(c.getAgendamento().toInterval());
			}
		}

		List<Interval> intervalosCandidatos = new ArrayList<Interval>(diferencas(intervalosAtendimento,
				intervalosOcupados));

		ordenarPorInicio(intervalosCandidatos);

		Duration duracaoRequerida = medico.calculaDuracaoConsulta(consulta);

		for (Interval candidato : intervalosCandidatos) {
			if (candidato.getEnd().isBefore(apartirDe)) {
				continue;
			}

			if (candidato.toDuration().isLongerThan(duracaoRequerida)) {
				// Se o intervalo começa depois ou em de apartirDe...
				if (candidato.getStart().isAfter(apartirDe)) {
					// ...agendamos apartir do começo do intervalo.
					return Agendamento.criarAgendamentoPorMedico(medico, candidato.getStart(), duracaoRequerida);

					// Se o intervalo começa antes de apartirDe e é possível
					// tomar um sub-intervalo...
				} else if (new Interval(apartirDe, duracaoRequerida).getEnd().isBefore(candidato.getEnd())) {
					// ...agendamos apartir de apartirDe.
					return Agendamento.criarAgendamentoPorEspecialidade(medico, apartirDe, duracaoRequerida);
				}
			}
		}

		// Não é possível agendar esta consulta para este médico, nesta data.
		throw new ErroAgendamento("Não foi possível criar um agendamento nesta data.");
	}

	public Agendamento criarAgendamento(Consulta consulta, Especialidade especialidade, DateTime apartirDe)
			throws ErroAgendamento {
		List<Medico> medicos = this.repositorioMedico.pesquisar(especialidade);
		if (medicos.isEmpty()) {
			throw new IllegalArgumentException("Não existem médicos na especialidade.");
		}

		Map<Medico, Agendamento> candidatos = new HashMap<Medico, Agendamento>();

		for (Medico m : medicos) {
			try {
				candidatos.put(m, criarAgendamento(consulta, m, apartirDe));
			} catch (ErroAgendamento e) {
				// Do nothing.
			}
		}

		if (candidatos.isEmpty()) {
			throw new ErroAgendamento("Não foi possível criar um agendamento nesta data.");
		}

		Medico proximoMedico = null;

		for (Medico m : candidatos.keySet()) {
			if (proximoMedico == null) {
				proximoMedico = m;
			} else if (candidatos.get(proximoMedico).getInicio().isAfter(candidatos.get(m).getInicio())) {
				proximoMedico = m;
			}
		}

		if (proximoMedico == null) {
			throw new IllegalStateException("Bug.");
		}

		return candidatos.get(proximoMedico);
	}

	private Set<Interval> diferencas(Set<Interval> intervalsA, Set<Interval> intervalsB) {
		Set<Interval> results = new HashSet<Interval>();

		// Garante que não há intervalos contínuos
		Set<Interval> intA = mesclarFronteiras(intervalsA);
		Set<Interval> intB = mesclarFronteiras(intervalsB);

		for (Interval a : intA) {
			// Coleta as sobreposicoes de B em A.
			Set<Interval> overlapings = new HashSet<Interval>();

			for (Interval b : intB) {
				if (a.overlaps(b)) {
					overlapings.add(b);
				}
			}

			// Intervalo nulo para marcar o inicio de A.
			Interval startMarker = new Interval(a.getStart(), a.getStart());
			overlapings.add(startMarker);

			// Intervalo nulo para marcar o final de A.
			Interval endMarker = new Interval(a.getEnd(), a.getEnd());
			overlapings.add(endMarker);

			// O resultado é o complemento da lista de sobreposicoes de B em A.
			results.addAll(lacunas(overlapings));
		}

		return results;
	}

	private Set<Interval> lacunas(Set<Interval> intervals) {
		List<Interval> sorted = new ArrayList<Interval>(mesclarFronteiras(intervals));
		ordenarPorInicio(sorted);

		Set<Interval> gaps = new HashSet<Interval>();

		Interval last = null;
		Interval current = null;

		Iterator<Interval> it = sorted.iterator();
		while (it.hasNext()) {
			current = it.next();

			if (last != null) {
				assert last.isBefore(current);
				assert last.gap(current).getStart().equals(last.getEnd());
				assert last.gap(current).getEnd().equals(current.getStart());

				gaps.add(last.gap(current));
			}

			last = current;
			current = null;
		}

		return gaps;
	}

	private Set<Interval> mesclarFronteiras(Set<Interval> intervals) {
		List<Interval> sorted = new ArrayList<Interval>(intervals);
		ordenarPorInicio(sorted);

		Stack<Interval> merged = new Stack<Interval>();

		Iterator<Interval> it = sorted.iterator();
		while (it.hasNext()) {
			Interval current = it.next();

			if (!merged.empty() && current.abuts(merged.peek())) {
				merged.push(new Interval(merged.pop().getStart(), current.getEnd()));
			} else {
				merged.push(current);
			}
		}

		return new HashSet<Interval>(merged);
	}

	private void ordenarPorInicio(List<Interval> intervals) {
		Collections.sort(intervals, new Comparator<Interval>() {
			public int compare(Interval a, Interval b) {
				return new CompareToBuilder().append(a.getStart(), b.getStart())
						.append(a.getEnd(), b.getEnd()).build();
			}
		});
	}
}